##import pygame.surfarray
from pygame.rect import Rect
from common import *
import time
from operator import getitem
class GameMap:
    ''' Game map object, describes a 2d map, and methods the player positions  '''
    def __init__(self,map,tile_w,tile_h,tiles,startingPositions=None,bases=[],bgcolor=(0,0,0)):
        self.map = map
        self.bgcolor = bgcolor
        self.cropMap()
        self.tiles = tiles
        self.tile_w = tile_w
        self.tile_h = tile_h
        # array to hold base objects
        self.bases = bases

        # Needed to make a copy of the surface using convert() to prevent
        # the array_colorkey() function to lock the surface permanently in HW mode,
        # is this a bug in array_colorkey?
        self.colorkey_mask = tiles.convert()
        self.colorkey = self.colorkey_mask.get_colorkey()
##        self.colorkey_mask = pygame.surfarray.array_colorkey(tiles.convert()) # Numeric array of colorkey mask
##                                                                              # to use for colission detection 
        self.startingPositions = startingPositions # ship starting positions

        # whole map pixel size rect
        self.rect = Rect(0,0,tile_w * self.map.shape[0], tile_h * self.map.shape[1])

    def cropMap(self):
        pass
##        ' cut down map size '
##        max_x,max_y = 0,0
##        for x in range(self.map.shape[0]-1,0,-1):
##            if filter(None,ravel(self.map[x,:])):
##                max_x = x
##                break
##        for y in range(self.map.shape[1]-1,0,-1):
##            if filter(None,ravel(self.map[:,y])):
##                max_y = y
##                break
##        self.map = self.map[:max_x+1,:max_y+1]

    def rectCollide(self,rect):
        ''' check if any corner of rect is inside a non empty tile and returns the first point that is'''
        tw,ty = self.tile_w,self.tile_h
        pc,map = self._pixelCollide,self.map
        for point in (rect.topleft, rect.topright, rect.bottomleft, rect.bottomright):
            map_x = int(point[0] / tw)
            map_y = int(point[1] / ty)
            try:
                if map_y <0 or map_x<0: continue
                if self.map[map_x,map_y] != 0:
                    if pc(point,map[map_x,map_y]):
                        return point
            except IndexError:
                continue
        return None

    def pointCollide(self,point):
        ''' check if any corner of rect is inside a non empty tile and returns the first point that is'''
        map_x = int(point[0] / self.tile_w)
        map_y = int(point[1] / self.tile_h)
        try:
            if map_y <0 or map_x<0: raise IndexError
            if not self.map[map_x,map_y] == 0:
                if self._pixelCollide(point,self.map[map_x,map_y]):
                    return point
        except IndexError:
            return None
        return None

    def _pixelCollide(self,point,tilenr):
        'return true if pixel at pos is solid'
        fine_w = point[0] % self.tile_w
        fine_h = point[1] % self.tile_h
        x,y = (fine_w,(self.tile_h * (tilenr-1))+fine_h)
        try:
            return self.colorkey_mask.get_at((x,y)) != self.colorkey
##            return self.colorkey_mask[x,y]
        except IndexError,err:
            sys.stderr.write("Something fishy happend:\n%s\n"%err)
            return 1
        
    def getMapXYAtPos(self,x,y):
        ''' Get XY Map grid cordinates of pixel pos ''' 
        return (int(x / self.tile_w),int(y / self.tile_h))
        
    def getTileType(self,x,y):
        ''' Return tile type nr at tile xy pos ''' 
        if y>0 and y<self.map.shape[1] and x>0 and x<self.map.shape[0]:
            return self.map[x,y]
        else:
            return 0

    def setTileType(self,x,y,tilenr):
        ''' set the tile type at the map pos (used by editor), if pos outside map grow the map to fit ''' 
        #self.map.shape[:2]
        if x<0 and y<0:
            sys.stderr.write("Error: tile pos negative\n")
            return
        map = self.map
        # Create a new array with new size if map pos is outside current size, could not use resize for some reason
        s = map.shape[:]
        new_x,new_y = map.shape[:]
        if x>=s[0]: new_x = x+1
        if y>=s[1]:    new_y = y+1
        if s != (new_x,new_y):
            newmap = zeros((new_x,new_y))
            s = map.shape[:]
            newmap[:s[0],:s[1]] = map[:s[0],:s[1]]
            map = newmap
        # set map pos tilenr
        map[x,y] = tilenr
        self.rect = Rect(0,0,self.tile_w * map.shape[0], self.tile_h * map.shape[1])

        self.map = map
        self.cropMap()

    def getRect(self):
        return self.rect

    def getSize(self):
        return self.map.shape
    
#-------------------------------------------------------
COL = 0
ROW = 1

class TiledLayer:
    ''' class for rendering a GameMap object to a viewport.
        There are a lot of strange solutions to get around pythons speed limitations.  '''

    # Initialivze layer set up map and tile graphics data. 
    def __init__(self, map):
        '''map is an numeric 2D array with int values 0-255. Optimized to only calculate
           new tiles to render when the tile pos changes'''
        self.map = map
        self.tiles = map.tiles
        #self.tiles = tiles
        self.sublayers = []

        #rects used by drawRect
        self.map_size = self.map.getSize()
        self.viewports = {}
        self.old_map_x = None
        self.old_map_y = None
        self.tile_rects = []
        self.tiles2render = []
        self.s_rects = []
        self.t_rects = []
        
        # pre calc all source rects, for speed, store them sequentially in an array. 
        # make index [0] be a clear rect as 0 should not be drawn
        self.tile_rects.append(Rect(0,0,0,0))
        for row in range(254):
            self.tile_rects.append(Rect(0, (row*self.map.tile_h),self.map.tile_w, self.map.tile_h))

        # pre generate all dest and source rects objects in an 2-d array as [map_y][map_x]
        self._preCalcRects()
        
    def _preCalcRects(self):
        ' fill the pre calc array with source rects and target rects where map tilenr is non zero  '
        # Create a list to hold all the rects
        self.rects = []
        # fill the pre calc array with source rects and target rects where map tilenr is non zero
        map = self.map
        matrix = map.map
        tile_rects = self.tile_rects
        for y in range(0,self.map_size[1]):
            for x in range(0,self.map_size[0]):
                try:
                    tilenr = int(matrix[x,y])
                except IndexError:
                    tilenr = 0
                    
                if tilenr:
                    s_rect = tile_rects[tilenr]
                    t_rect = Rect(x*map.tile_w,y*map.tile_h,map.tile_w,map.tile_h)
                    self.rects.append((s_rect,t_rect))
                else:
                    self.rects.append(None)

    def forceRecalc(self):
        ' forces all precalced values to be recalculated, used if map data has changed(edit) '
        if self.map_size != self.map.getSize():
            self.map_size = self.map.getSize()

        self._preCalcRects()
        for id in self.viewports.keys():
            self.viewports[id] = [None for val in self.viewports[id]] #set all values to none

    #  Link the position of this layer to another layer, w/ scale ratio
    def link(self, to_tl, ratio):
        'TiledLayer to_tl, float ratio'
        self.sublayers.append({'layer':to_tl,'ratio':ratio})


    # Render layer to the specified surface 
    def render(self, viewport):
        ' Render layer to screen , viewport is a rect describing which part of the map to render and screen offset'
        screen = viewport.screen
        # get the precalced values for the view port so we dont break optimization
        if self.viewports.has_key(viewport.id):
            self.old_map_x, self.old_map_y ,self.tiles2render,self.s_rects,self.t_rects = self.viewports[viewport.id]
        x,y = viewport[0],viewport[1]
        # render all linked sub layers
        for layer in self.sublayers:
            # Calculate a new view port for the linked layers(apply ratio to cords)
            viewport[0] = x*layer['ratio']
            viewport[1] = y*layer['ratio']
            layer['layer'].render(viewport)
        viewport[0] = x
        viewport[1] = y

        self.__render(screen,viewport)
        # save the viewports precalced values
        self.viewports[viewport.id] = (self.old_map_x, self.old_map_y ,self.tiles2render,self.s_rects,self.t_rects)
        

    def __render(self, screen,viewport):
        'Draws itself to the surface screen'
        # Calculate which part of the map to draw 
        map_x = int((viewport[0]) / self.map.tile_w)
        map_y = int((viewport[1]) / self.map.tile_h)
        map_max_x = int(viewport.right / self.map.tile_w)+2
        map_max_y = int(viewport.bottom / self.map.tile_h)+2
        
        # only rerender the surface buffer if we are on a new tile
        if self.old_map_x != map_x or self.old_map_y != map_y:
            self.getTiles2Render(map_x,map_y,map_max_x,map_max_y)

        self.old_map_x = map_x
        self.old_map_y = map_y

        #  Calculate delta pos to add to precalced rects to render on screen
        x = -viewport[0] 
        y = -viewport[1] 

        self.drawTiles(screen,x,y)

    def getTiles2Render(self,map_x,map_y,map_max_x,map_max_y):
        ''' Precalc all rects of the map to be blitted, so we dont need
            to loop thrue the whole map array every time to get tiles to
            blit, just once every time we get into a new map tile pos'''
        # Slice the tiles to be displayed out of the precalced array and reshape array to one dim
        # and filter out relevant rects(Not None)
        # also keep map cords positive and below max
        tiles = self.rects[self.getIndex(map_x,map_y):
                           self.getIndex(map_max_x,map_max_y)]
        tiles = filter(None,tiles)
        self.tiles2render = tiles

    def drawTiles(self,screen,x,y):
        'Draws all tiles in tiles2render to screen'
        blit = screen.blit
        tiles = self.tiles
        for s_rect,d_rect in self.tiles2render:
            blit(tiles,d_rect.move(x,y),s_rect)
##        screen.mblit(self.tiles,self.s_rects,self.t_rects,(x,y))

    def getIndex(self,tile_col,tile_row):
        'convert a col,row pos to a index in the flat list'
        max_row = min(max(0,tile_row),self.map_size[ROW])
        max_col = min(max(0,tile_col),self.map_size[COL])
        return max_row * self.map_size[COL] + max_col

#-----------------------------------------------------------

class Base:
    ' Class for landing bases'
    charge_sound = None
    def __init__(self,tile_types):
        'tile types is equal to map tile type(color index)'
        self.tile_types = tile_types
        #Create fake sound channle obj for first time
        self.channel = None
        self.changed = 0
    def docked(self,ship,dt):
        # Play chrge sound if ship values changed
        if self.changed and self.charge_sound:
            if self.channel == None or not self.channel.get_busy():
                self.channel = self.charge_sound.play()

class HomeBase(Base):
    ' Class for landing bases'
    def docked(self,ship,dt):
        if ship:
            changed = 0
            changed |= ship.changeLife(10*dt)
            changed |= ship.changeFuel(10*dt)
            if ship.owner:
                changed |= ship.changePrimAmmo(10*dt)
                changed |= ship.changeSecAmmo(1*dt)
        self.changed = changed
        Base.docked(self,ship,dt)
#-----------------------------------------------------------
class FuelBase(Base):
    ' Class for landing bases'
    def docked(self,ship,dt):
        if ship:
            self.changed = ship.changeFuel(10*dt)
            Base.docked(self,ship,dt)

#-----------------------------------------------------------

class AmmoBase(Base):
    ' Class for landing bases'
    def docked(self,ship,dt):
        if ship and ship.owner:
            changed = 0
            changed |= ship.changePrimAmmo(10*dt)
            changed |= ship.changeSecAmmo(1*dt)
            self.changed = changed
            Base.docked(self,ship,dt)


#--------------------------------------------------------------------
